// Copyright (c) 2025 Shenzhen Kaihong Digital Industry Development Co., Ltd.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::{path::{Path, PathBuf}, str::FromStr};

use lazy_static::lazy_static;
use log::warn;
use tokio::fs::ReadDir;

use crate::Pkg;

use super::pkg_stat::{StatSpec, StatSpecParseError};

pub const PKG_CONFIG_FILE_NAME: &str = "config.toml";

lazy_static! {
    pub static ref CONFIG_ROOT: String = std::env::var("CONFIG_ROOT")
        .unwrap_or(concat!("/etc/", env!("CARGO_CRATE_NAME")).to_string());
    pub static ref PKGS_CONFIG_DIR: String = [CONFIG_ROOT.as_str(), "/pkgs"].concat();
    pub static ref SYSTEM_CONFIG_PATH: String = [CONFIG_ROOT.as_str(), "/config.toml"].concat();

    pub static ref PKGS_ROOT: PathBuf = std::env::var("PKGS_ROOT")
        .map(|p| std::fs::canonicalize(p).expect("PKGS_ROOT is invalid"))
        .unwrap_or(PathBuf::from("/pkg"));

    pub static ref CACHE_ROOT: PathBuf = std::env::var("CACHE_ROOT")
        .map(|p| std::fs::canonicalize(p).expect("CACHE_ROOT is invalid"))
        .unwrap_or(dirs::cache_dir().unwrap_or(PathBuf::from("~/.cache")));
}

pub struct FindAvailablePkgs {
    read_dir: ReadDir,
}

pub struct FindInstalledPkgs {
    read_dir: ReadDir,
}

pub struct FindPatches {
    read_dir: ReadDir,
}

#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum StatFileNameParseError {
    #[error("not '.stat' extension")]
    ExtensionNotMatch,
    #[error(transparent)]
    StatSpecParseError(#[from] StatSpecParseError),
}

pub async fn find_available_pkgs() -> Result<FindAvailablePkgs, std::io::Error> {
    FindAvailablePkgs::new(PKGS_CONFIG_DIR.as_str()).await
}

pub async fn find_installed_pkgs() -> Result<FindInstalledPkgs, std::io::Error> {
    FindInstalledPkgs::new(PKGS_ROOT.as_path()).await
}

pub async fn find_patches<P: AsRef<Path>>(patches_dir: P) -> Result<FindPatches, std::io::Error> {
    FindPatches::new(patches_dir).await
}

pub fn pkg_config_path(pkg: &Pkg) -> PathBuf {
    let mut path = PathBuf::from(PKGS_CONFIG_DIR.as_str());
    path.push(pkg.to_string());
    path.push(PKG_CONFIG_FILE_NAME);
    path
}

pub fn pkg_stat_path(spec_str: &str) -> PathBuf {
    let mut path = PathBuf::new();
    path.push(PKGS_ROOT.as_path());
    path.push(pkg_stat_file_name(spec_str));
    path
}

pub fn pkg_stat_file_name(spec_str: &str) -> String {
    format!("{}.stat", spec_str)
}

pub async fn collect_installed_pkg_specs() -> Result<Vec<String>, std::io::Error> {
    let mut find_pkgs = find_installed_pkgs().await?;
    let mut spec_strings = Vec::new();
    while let Some((file_name, stat_spec)) = find_pkgs.next().await? {
        if !stat_spec.impure {
            spec_strings.push(file_name.strip_suffix(".stat").unwrap().to_string());
        }
    }
    Ok(spec_strings)
}

impl FindAvailablePkgs {
    pub async fn new<P: AsRef<Path>>(path: P) -> Result<Self, tokio::io::Error> {
        Ok(Self { read_dir: tokio::fs::read_dir(path).await? })
    }

    pub async fn next(&mut self) -> Result<Option<Pkg>, std::io::Error> {
        while let Some(entry) = self.read_dir.next_entry().await? {
            let ty = entry.file_type().await?;
            if !ty.is_dir() {
                continue;
            }

            let path = entry.path();
            if path.join(PKG_CONFIG_FILE_NAME).is_file() {
                let s = path.file_name().unwrap().to_string_lossy();
                match Pkg::from_str(&s) {
                    Ok(pkg) => return Ok(Some(pkg)),
                    Err(_) => {
                        warn!("non-pkg directory '{}' exists", path.display());
                    },
                }
            }
        }
        Ok(None)
    }
}

impl FindInstalledPkgs {
    pub async fn new<P: AsRef<Path>>(path: P) -> Result<Self, tokio::io::Error> {
        Ok(Self { read_dir: tokio::fs::read_dir(path).await? })
    }

    pub async fn next(&mut self) -> Result<Option<(String, StatSpec)>, std::io::Error> {
        while let Some(entry) = self.read_dir.next_entry().await? {
            let ty = entry.file_type().await?;
            if !ty.is_file() {
                continue;
            }

            let file_name = entry.file_name().to_string_lossy().to_string();
            match Self::parse_stat_file_name(&file_name) {
                Ok(stat_spec) => {
                    return Ok(Some((file_name, stat_spec)));
                },
                Err(err) => match err {
                    StatFileNameParseError::ExtensionNotMatch => {},
                    err => {
                        warn!("invalid pkg stat '{}' exists:\n{:?}", file_name, err);
                    }
                },
            }
        }
        Ok(None)
    }

    fn parse_stat_file_name(file_name: &str) -> Result<StatSpec, StatFileNameParseError> {
        const EXTENSION: &str = ".stat";

        if !file_name.ends_with(EXTENSION) {
            return Err(StatFileNameParseError::ExtensionNotMatch);
        }

        Ok(StatSpec::from_str(&file_name[..file_name.len() - EXTENSION.len()])?)
    }
}

impl FindPatches {
    pub async fn new<P: AsRef<Path>>(path: P) -> Result<Self, tokio::io::Error> {
        Ok(Self { read_dir: tokio::fs::read_dir(path).await? })
    }

    pub async fn next(&mut self) -> Result<Option<PathBuf>, std::io::Error> {
        while let Some(entry) = self.read_dir.next_entry().await? {
            let ty = entry.file_type().await?;
            if !ty.is_file() {
                continue;
            }

            let path = entry.path();
            let Some(extension) = path.extension() else {
                continue;
            };
            if extension != "patch" {
                continue;
            }
            
            return Ok(Some(path));
        }
        Ok(None)
    }
}

#[cfg(test)]
mod test {
    use sha2::{Digest, Sha256};

    use crate::{database::{local::{FindInstalledPkgs, StatFileNameParseError}, pkg_stat::{StatSpec, StatSpecParseError}}, utils::sha256_to_string, Arch, Pkg, Version};

    #[test]
    fn test_parse_stat_file_name() {
        let mut hasher = Sha256::new();
        hasher.update("random");
        let sha256 = hasher.finalize();

        let file_name = format!("{}-test-0.1-x86_64.stat", sha256_to_string(&sha256));
        assert_eq!(
            FindInstalledPkgs::parse_stat_file_name(&file_name),
            Ok(StatSpec {
                sha256,
                pkg: Pkg::new("test".to_string(), Version::from_major_minor(0, 1)),
                arch: Arch::X86_64,
                impure: false,
            })
        );

        let file_name = format!("{}-test-0.1-x86_64-impure.stat", sha256_to_string(&sha256));
        assert_eq!(
            FindInstalledPkgs::parse_stat_file_name(&file_name),
            Ok(StatSpec {
                sha256,
                pkg: Pkg::new("test".to_string(), Version::from_major_minor(0, 1)),
                arch: Arch::X86_64,
                impure: true,
            })
        );

        assert_eq!(
            FindInstalledPkgs::parse_stat_file_name("no_extension"),
            Err(StatFileNameParseError::ExtensionNotMatch)
        );
        assert_eq!(
            FindInstalledPkgs::parse_stat_file_name("test-0.1-x86_64.stat"),
            Err(StatFileNameParseError::StatSpecParseError(StatSpecParseError::BadFormat))
        );
    }
}